/***********************************************************************
 * Map class
 * 
 * Implements maps
 *
 * Signature:
 *  public static Map empty (int x, int y);
 *  public static Map artifact (int x, int y, Artifact a);
 *  public static Map merge (Map m1, Map m2);
 *  public Coord size ();
 *  public boolean isEmpty ();
 *  public Artifact firstArtifact ();
 *  public Map restArtifacts ();
 *  public boolean wellFormed ();
 *  public Option<Artifact> findArtifactByName (String n);
 *  public Option<Artifact> findArtifactByPosition (Coord c);
 *  public Map removeArtifact (Artifact a);
 *  public String toString ();
 *  public FuncIterator<Artifact> getFuncIterator ();
 *  public Iterator<Artifact> iterator ();
 *  public List<String> picture ();
 ***********************************************************************/

import java.util.Iterator;
import java.lang.Iterable;

import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONString;
import org.json.JSONStringer;

public abstract class Map implements Iterable<Artifact>, JSONString {
  
  public static Map empty(int x, int y) {
    return new MapEmpty(x, y);
  }
  
  public static Map artifact(int x, int y, Artifact a) {
    if (!a.position().within(Coord.create(0, 0), Coord.create(x - 1, y - 1)))
      throw new IllegalArgumentException("Map.artifact()");
    return new MapArtifact(x, y, a);
  }
  
  public static Map merge(Map m1, Map m2) {
    if (!m1.size().equals(m2.size()))
      throw new IllegalArgumentException("Map.merge()");
    Map result = new MapMerge(m1, m2);
    if (!result.wellFormed())
      throw new IllegalArgumentException("Map.merge()");
    return result;
  }
  
  public abstract Coord size();
  
  public abstract boolean isEmpty();
  
  public abstract Artifact firstArtifact();
  
  public abstract Map restArtifacts();
  
  public abstract boolean wellFormed();
  
  public abstract Option<Artifact> findArtifactByName(String n);
  
  public abstract Option<Artifact> findArtifactByPosition(Coord c);
  
  /*
   * public abstract Map moveArtifact (Movable a, int dx, int dy);
   * 
   * public abstract Map replaceArtifact (Artifact a, Artifact b);
   */

  public abstract Map removeArtifact(Artifact a);
  
  public abstract String toString();
  
  public abstract FuncIterator<Artifact> getFuncIterator();
  
  public Iterator<Artifact> iterator() {
    return IteratorAdapter.create(this.getFuncIterator());
  }
  
  public List<String> picture() {
    int xmax = this.size().xPos();
    int ymax = this.size().yPos();
    
    if (xmax < 1 || ymax < 1)
      throw new IllegalArgumentException("Size of drawing < 1");
    
    char drawing[][] = new char[xmax][ymax];
    
    for (int i = 0; i < xmax; i++)
      for (int j = 0; j < ymax; j++)
        drawing[i][j] = '.';
    
    for (Artifact art : this)
      drawing[art.position().xPos()][art.position().yPos()] = art.display();
    
    List<String> result = List.empty();
    for (int j = 0; j < ymax; j++) {
      char line[] = new char[xmax * 2];
      for (int i = 0; i < xmax; i++) {
        line[2 * i] = ' ';
        line[2 * i + 1] = drawing[i][j];
      }
      result = List.cons(new String(line), result);
    }
    return result;
  }

  public static Map fromJSON(JSONObject json) {
    try {
      if (json.getString("className").equals("MapEmpty")) {
        return Map.empty(json.getInt("xsize"), json.getInt("ysize"));
      } else if (json.getString("className").equals("MapArtifact")) {
        return Map.artifact(json.getInt("xsize"), json.getInt("ysize"), Artifact.fromJSON(json.getJSONObject("artifact")));
      } else if (json.getString("className").equals("MapMerge")) {
        return Map.merge(Map.fromJSON(json.getJSONObject("map1")), Map.fromJSON(json.getJSONObject("map2")));
      } else {
        System.err.println("\"" + json.toString() + "\" does not represent a Map.");
        throw new RuntimeException("Cannot parse JSON, it is not a Map!");
      }
    } catch (JSONException e) {
      e.printStackTrace();
      throw new RuntimeException("Unrecoverable JSON error, see above stack trace");
    }
  }
  
}

class MapEmpty extends Map {
  
  private int xsize;
  private int ysize;
  
  public MapEmpty(int x, int y) {
    xsize = x;
    ysize = y;
  }
  
  public Coord size() {
    return Coord.create(xsize, ysize);
  }
  
  public boolean isEmpty() {
    return true;
  }
  
  public Artifact firstArtifact() {
    throw new Error("empty().firstArtifact() undefined");
  }
  
  public Map restArtifacts() {
    throw new Error("empty().restArtifacts() undefined");
  }
  
  public boolean wellFormed() {
    return true;
  }
  
  public Option<Artifact> findArtifactByName(String n) {
    return Option.none();
  }
  
  public Option<Artifact> findArtifactByPosition(Coord c) {
    return Option.none();
  }
  
  /*
   * public Map moveArtifact (Movable a, int dx, int dy) { return this; }
   * 
   * public Map replaceArtifact (Artifact a, Artifact b) { return this; }
   */

  public Map removeArtifact(Artifact a) {
    return this;
  }
  
  public String toString() {
    return "";
  }
  
  public FuncIterator<Artifact> getFuncIterator() {
    return new FuncIteratorEmpty();
  }

  @Override
  public String toJSONString() {
    try {
      return new JSONStringer()
                        .object() 
                          .key("className")
                          .value("MapEmpty")
                          .key("xsize")
                          .value(this.xsize)
                          .key("ysize")
                          .value(this.ysize)
                        .endObject().toString();
    } catch (JSONException e) {
      e.printStackTrace();
      throw new RuntimeException("Unrecoverable JSON error, see above stack trace.");
    }
  }
  
}

class FuncIteratorEmpty implements FuncIterator<Artifact> {
  
  public FuncIteratorEmpty() {
  }
  
  public boolean hasElement() {
    return false;
  }
  
  public Artifact current() {
    throw new java.util.NoSuchElementException("Iterator empty");
  }
  
  public FuncIterator<Artifact> advance() {
    throw new java.util.NoSuchElementException("Iterator empty");
  }
  
}

class MapArtifact extends Map {
  
  private int xsize;
  private int ysize;
  private Artifact artifact;
  
  public MapArtifact(int x, int y, Artifact a) {
    xsize = x;
    ysize = y;
    artifact = a;
  }
  
  public Coord size() {
    return Coord.create(xsize, ysize);
  }
  
  public boolean isEmpty() {
    return false;
  }
  
  public Artifact firstArtifact() {
    return artifact;
  }
  
  public Map restArtifacts() {
    return Map.empty(this.size().xPos(), this.size().yPos());
  }
  
  public boolean wellFormed() {
    return true;
  }
  
  public Option<Artifact> findArtifactByName(String n) {
    if (artifact.name().equals(n))
      return Option.some(artifact);
    return Option.none();
  }
  
  public Option<Artifact> findArtifactByPosition(Coord c) {
    if (artifact.position().equals(c))
      return Option.some(artifact);
    return Option.none();
  }
  
  /*
   * public Map moveArtifact (Movable a, int dx, int dy) { if (a==artifact) {
   * Coord pos = a.position(); if
   * (a.position().move(dx,dy).within(Coord.create(0,0),
   * this.size().move(-1,-1))) return Map.artifact(this.size().xPos(),
   * this.size().yPos(), a.move(dx,dy)); throw new
   * IllegalArgumentException("Moving out of bounds"); } return this; }
   * 
   * public Map replaceArtifact (Artifact a, Artifact b) { if (a==artifact)
   * return Map.artifact(this.size().xPos(), this.size().yPos(), b); return
   * this; }
   */

  public Map removeArtifact(Artifact a) {
    if (a == artifact)
      return Map.empty(this.size().xPos(), this.size().yPos());
    return this;
  }
  
  public String toString() {
    return artifact.toString();
  }
  
  public FuncIterator<Artifact> getFuncIterator() {
    return new FuncIteratorArtifact(artifact);
  }

  @Override
  public String toJSONString() {
    try {
      return new JSONStringer()
                        .object() 
                          .key("className")
                          .value("MapArtifact")
                          .key("xsize")
                          .value(this.xsize)
                          .key("ysize")
                          .value(this.ysize)
                          .key("artifact")
                          .value(this.artifact)
                        .endObject().toString();
    } catch (JSONException e) {
      e.printStackTrace();
      throw new RuntimeException("Unrecoverable JSON error, see above stack trace.");
    }
  }
  
}

class FuncIteratorArtifact implements FuncIterator<Artifact> {
  
  private Artifact artifact;
  
  public FuncIteratorArtifact(Artifact a) {
    artifact = a;
  }
  
  public boolean hasElement() {
    return true;
  }
  
  public Artifact current() {
    return this.artifact;
  }
  
  public FuncIterator<Artifact> advance() {
    return new FuncIteratorEmpty();
  }
  
}

class MapMerge extends Map {
  
  private Map map1;
  private Map map2;
  
  public MapMerge(Map m1, Map m2) {
    map1 = m1;
    map2 = m2;
  }
  
  public Coord size() {
    return map1.size();
  }
  
  public boolean isEmpty() {
    return map1.isEmpty() && map2.isEmpty();
  }
  
  public Artifact firstArtifact() {
    if (map1.isEmpty())
      return map2.firstArtifact();
    else
      return map1.firstArtifact();
  }
  
  public Map restArtifacts() {
    if (map1.isEmpty())
      return map2.restArtifacts();
    else
      return Map.merge(map1.restArtifacts(), map2);
  }
  
  public boolean wellFormed() {
    if (map1.isEmpty())
      return map2.wellFormed();
    Artifact a = map1.firstArtifact();
    if (this.restArtifacts().findArtifactByPosition(a.position()).isNone()
        && this.restArtifacts().findArtifactByName(a.name()).isNone())
      return this.restArtifacts().wellFormed();
    return false;
  }
  
  public Option<Artifact> findArtifactByName(String n) {
    Option<Artifact> result;
    result = map1.findArtifactByName(n);
    if (result.isNone())
      return map2.findArtifactByName(n);
    return result;
  }
  
  public Option<Artifact> findArtifactByPosition(Coord c) {
    Option<Artifact> result = map1.findArtifactByPosition(c);
    if (result.isNone())
      return map2.findArtifactByPosition(c);
    return result;
  }
  
  /*
   * public Map moveArtifact (Movable a, int dx, int dy) { return
   * Map.merge(map1.moveArtifact(a,dx,dy), map2.moveArtifact(a,dx,dy)); }
   * 
   * 
   * public Map replaceArtifact (Artifact a, Artifact b) { return
   * Map.merge(map1.replaceArtifact(a,b), map2.replaceArtifact(a,b)); }
   */

  public Map removeArtifact(Artifact a) {
    return Map.merge(map1.removeArtifact(a), map2.removeArtifact(a));
  }
  
  public String toString() {
    return map1.toString() + " " + map2.toString();
  }
  
  public FuncIterator<Artifact> getFuncIterator() {
    return new FuncIteratorMerge(map1.getFuncIterator(), map2.getFuncIterator());
  }

  @Override
  public String toJSONString() {
    try {
      return new JSONStringer()
                        .object() 
                          .key("className")
                          .value("MapMerge")
                          .key("map1")
                          .value(this.map1)
                          .key("map2")
                          .value(this.map2)
                        .endObject().toString();
    } catch (JSONException e) {
      e.printStackTrace();
      throw new RuntimeException("Unrecoverable JSON error, see above stack trace.");
    }
  }
  
}

class FuncIteratorMerge implements FuncIterator<Artifact> {
  
  private FuncIterator<Artifact> iter1;
  private FuncIterator<Artifact> iter2;
  
  public FuncIteratorMerge(FuncIterator<Artifact> i1, FuncIterator<Artifact> i2) {
    iter1 = i1;
    iter2 = i2;
  }
  
  public boolean hasElement() {
    return (iter1.hasElement() || iter2.hasElement());
  }
  
  public Artifact current() {
    if (iter1.hasElement())
      return iter1.current();
    else
      return iter2.current();
  }
  
  public FuncIterator<Artifact> advance() {
    if (iter1.hasElement())
      return new FuncIteratorMerge(iter1.advance(), iter2);
    return iter2.advance();
  }
  
}
